﻿using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

using Outfish.JavaScript;

namespace Outfish {

	/// <summary>
	/// A Document Object Model of Javascript literals.
	/// </summary>
	public class JavaScriptDocument : WebDocument {
	
		#region constructors
	
		/// <summary>
		/// Creates a JavaScriptDocument inserting a phantom root-script tag, using scriptContent as the Javascript content
		/// </summary>
		/// <param name="content"></param>
		public JavaScriptDocument( string content ):base(content){
			this.DocumentNode = BuildPhantomRootNode( content, this );
			BuildStructureTree( this.DocumentNode );
		}

		#endregion

		static public void BuildStructureTree(ContainerNode documentNode ){

			var tokenList = BuildTokenList(documentNode);

			if( tokenList.Count == 0 ){ return; }

			Stack<int> openStack = new Stack<int>();
			
			int beginOfTextNode = tokenList[0].Begin; 

			ContainerNode cur = documentNode;
			for(int i=0;i<tokenList.Count;++i){
				JsToken prev = i>0 ? tokenList[i-1] : null; // can't just use last token because function skips over a token...
				JsToken token = tokenList[i];
				JsToken next = (i+1) < tokenList.Count ? tokenList[i+1] : null;
				
				// semi-colon
				if( token.Type == JsTokenType.Semicolon ){
					ConsumeText(cur, ref beginOfTextNode, token.End); // include semicolon in text block
				}

				// turn some tokens into their own node
				if( token.Type == JsTokenType.Keyword ){
					// create node for it
					ConsumeText(cur, ref beginOfTextNode, token.Begin); 
					var keywordHeader = new HeaderFooter(cur.Source, token.Begin, token.End, token.Text, HeaderFooterType.Single);
					var keywordNode = new ContainerNode( documentNode.Document, keywordHeader, new Dictionary<string,string>() );
					cur.AddChild( keywordNode );
					beginOfTextNode = token.End;
					
					// name function
					if( token.Text=="function" && next != null && next.Type == JsTokenType.Identifier ){
						IdentifyNode( keywordNode, next.Text );
						++i; // skip id token
						beginOfTextNode = next.End;
					}
				}


				// turn literals into their own (named) node
				if(	token.Type == JsTokenType.StringLiteral
					||	token.Type == JsTokenType.NumberLiteral
					||	token.Type == JsTokenType.BooleanLiteral
					||	token.Type == JsTokenType.RegexLiteral
				){
					// create node for it
					ConsumeText(cur, ref beginOfTextNode, token.Begin); 
					string name = token.Type.ToString(); 
					name = name.Substring( 0, name.Length - 7 ); // strip 'Literal'
					var literalHeader = new HeaderFooter(cur.Source, token.Begin, token.End, name, HeaderFooterType.Single);
					var literalNode = new ContainerNode( documentNode.Document, literalHeader, new Dictionary<string,string>() );
					cur.AddChild( literalNode );
					beginOfTextNode = token.End;
					
					IdentifyContainerNode( i, literalNode, tokenList );
					
				}

				if( token.Type != JsTokenType.Bracket ){ continue; }
				
				if( IsOpen( token ) ){
					openStack.Push(i);
					
					// add previous text node
					ConsumeText(cur, ref beginOfTextNode, token.Begin);
					
					// add new node
					var name = BracketName(token.Text);
					var header = new HeaderFooter(cur.Source, token.Begin, token.End, "block", HeaderFooterType.Head );
					var blockNode = new ContainerNode( cur.Document, header, new Dictionary<string,string>{ {"class", name} }  );
					cur.AddChild( blockNode );
					
					IdentifyContainerNode( i, blockNode, tokenList );
					
					// set new text-node start
					beginOfTextNode = token.End;
					
					// move down to new node
					cur = blockNode;
					
				} else {
				
					// add previous text node
					ConsumeText(cur, ref beginOfTextNode, token.Begin);
				
					// validate, pop, validate
					if( openStack.Count > 0 ){ 
						var openIndex = openStack.Pop();
						if( !Match( tokenList[openIndex], token ) ){ throw new Exception("mismatch"); }
					} else {
						// throw new Exception("missing close token."); }
					}

					// add footer
					cur.Footer = new HeaderFooter(cur.Source, token.Begin, token.End, "block", HeaderFooterType.Tail );
					
					FixStructure( cur );
					
					// move up to parent
					cur = cur.ParentNode;
					
					// happens if we have an extra close, 
					// causes exception if we try to keep going
					if( cur == null ){ break; } 
					
					// set new text-node start
					beginOfTextNode = token.End;
					
				}
			}
			
			// add previous text node
			ConsumeText(cur, ref beginOfTextNode, documentNode.Footer.Begin); // end
			
			FixStructure( cur );
		}

		#region static private methods

		/// <summary>
		/// try to name the node using '=' or ':'
		/// </summary>
		static void IdentifyContainerNode( int i, ContainerNode node, List<JsToken> tokens ){
			
			if( i<2 ){ return; }
			
			string prevText = tokens[i-1].Text;
			if( prevText != "=" && prevText != ":" ){ return; }
			
			// get name
			JsToken identityToken = tokens[i-2];
			string identity = identityToken.Text;
			if( identityToken.Type == JsTokenType.StringLiteral ){
				identity = identity.Substring( 1, identity.Length-2 ); // strip ending quotes
			}
			// set it
			IdentifyNode( node, identity );
		
		}

		static void MoveNextSiblingToChild( List<HtmlNode> nodes, int pivotIndex ){
			if( nodes.Count <= pivotIndex + 1 ){ return; }
			ContainerNode pivotNode = nodes[pivotIndex] as ContainerNode;
			if( pivotNode == null ){ throw new Exception("pivot node is null"); }
			pivotNode.AddChild( nodes[pivotIndex+1] );
			nodes.RemoveAt(pivotIndex+1);
		}

		static string BracketName( string text ){
			switch( text ){
				case "[":
				case "]": return SquareName;
				case "{":
				case "}": return CurlyName;
				case "(":
				case ")": return RoundName;
			}
			throw new Exception("Bracket name for "+text);
		}

		static void ConsumeText(ContainerNode node, ref int begin, int end){
			// add previous text node
			if( begin < end ){
				TextNode textNode = new TextNode(node.Document.Source, begin, end);
				if( textNode.IsWhiteSpace == false ){
					node.AddChild(textNode);
				}
				begin = end; // advance
			}
		
		}

		static void FixStructure( ContainerNode node ){

			// maybe rename nodes to "condition" and "body"  or att attributes
			// see how variable names and functions pan out

			var isRound = AttributeSelector.ContainsClass( RoundName );
			var isCurly = AttributeSelector.ContainsClass( CurlyName );

			for(int i=0; i<node.ChildNodes.Count; ++i){
				HtmlNode child = node.ChildNodes[i];
				
				// restructure
				switch( child.Name ){
						case "function":
						case "while":
						case "with":  
						case "switch":
						case "for":
						case "if":
							// attach (...)
							if( Test( node, i+1, isRound.IsMatch )==false ){ break; }
							MoveNextSiblingToChild( node.ChildNodes, i );
							goto case "else"; // need body
						case "do":
						case "try":
						case "finally":
						case "else":
							// attach {}
							if( Test( node, i+1, isCurly.IsMatch )==false ){ break; }
							MoveNextSiblingToChild( node.ChildNodes, i );
							break;
				}
			}

		}

		static void IdentifyNode( ContainerNode node, string name ){
			// option 1 - harder to write/read css selector rules
//			node.SetAttr( "name", name ); 
			// option 2 - violates 1 id per doc rule.
			node.SetAttr( "id", name );
		}

		static bool Test( HtmlNode parent, int childIndex, Predicate<HtmlNode> pred ){
			if( childIndex < 0 || parent.ChildNodes.Count <= childIndex ){ return false; }
			var node = parent.ChildNodes[ childIndex ];
			return pred( node );
		}

		static bool IsOpen( JsToken t ){
			return t.Type == JsTokenType.Bracket && "[({".Contains( t.Text );
		}
	
		static bool Match( JsToken op, JsToken cl ){
			return "[]{}()".Contains( op.Text + cl.Text );
		}

		static List<JsToken> BuildTokenList(ContainerNode node){

			var tokens = new JsLexer().GetTokens( 
				node.Document.Source
				, node.Header.End		// begin
				, node.Footer.Begin 		// end
			);
			
			// simple way to make list but it crashes if we hit invalid token
			// var tokenList = tokens.ToList();
			
			// more complicated but it survives invalid token exceptions
			List<JsToken> tokenList = new List<JsToken>();
			try{ 
			foreach( var token in tokens ){ tokenList.Add( token ); }
			}catch{}
			return tokenList;
		}

		// builds/returns a phantom root node for string content
		static ContainerNode BuildPhantomRootNode(string source,WebDocument doc){
			// create header at begining, footer at end
			HeaderFooter header = new HeaderFooter( source, 0, 0, "script", HeaderFooterType.Head );
			HeaderFooter footer = new HeaderFooter( source, source.Length, source.Length, "script",HeaderFooterType.Tail);

			// make node
			ContainerNode rootNode = new ContainerNode( doc,header,new Dictionary<string,string>{{"type","text/javascript"}} );
			rootNode.Footer = footer;
			return rootNode;
		}

		const string RoundName = "round";
		const string CurlyName = "curly";
		const string SquareName = "square";

		#endregion

	}
	
}
